import { dom, EventHandler, Nodes } from '@knno/dom';
import { pulldownStyle } from './pulldown.css';
import SVGArrowDown from '@material-design-icons/svg/filled/arrow_downward.svg';
import { getOffsetX, rem2px } from '../../tools/utils';
import { loadingDot } from '../loading/loading';
import { Container } from '../interface';
import { DATA } from '../symbol';

export interface PulldownEventMap {
	scrollbottom: Event;
}

export type PulldownEventKey = keyof PulldownEventMap;
export type PulldownEventHandler<T extends PulldownEventKey, O extends Nodes> = (
	this: O,
	event: PulldownEventMap[T]
) => void;

export interface Pulldown {
	on<K extends keyof PulldownEventMap>(
		event: K,
		listener: PulldownEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;

	off<K extends keyof PulldownEventMap>(
		event: K,
		listener: PulldownEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

export class Pulldown extends Container {
	private [DATA]: { mouseScroll: boolean } = { mouseScroll: true };
	private refreshHandler: ((this: this) => Promise<void>) | null = null;

	constructor() {
		const arrow = dom.html(SVGArrowDown);
		const ld = loadingDot();
		const info = dom.div('info').append(arrow);
		const box = dom.div('content');
		const anchor = dom.div('anchor');
		super(dom.div(pulldownStyle).append(info, box, anchor));
		// this[DATA] = { box };

		let moved = false;

		function move(pos: number) {
			if (pos > 0) moved = true;
			box.css('transform', `translateY(${pos}px)`);
			info.css('height', `${pos - 1 > 0 ? pos - 1 : 0}px`);
		}

		function moveAni(pos: number, cb?: () => void) {
			box.css('transition', 'transform 0.2s');
			info.css('transition', 'height 0.2s');
			move(pos);
			setTimeout(() => {
				box.css('transition', '');
				info.css('transition', '');
				cb?.();
			}, 200);
		}

		function onCancel() {
			downID = null;
			isTouch = false;
			refreshing = false;
			top = null;
			arrow.attr('class', null);
			moveAni(0, () => info.html('').append(arrow));
		}

		const doRefresh: PulldownEventHandler<keyof PulldownEventMap, this> = (evt) => {
			if (downID === null) return;
			if (ready && typeof this.refreshHandler === 'function') {
				evt.preventDefault();
				evt.stopImmediatePropagation();
				downID = null;
				refreshing = true;
				info.html('').append(ld);
				moveAni(maxH);
				this.refreshHandler()
					.then(() => {
						onCancel();
					})
					.finally(() => {
						onCancel();
					});
			} else {
				onCancel();
			}
		};
		// const noScroll = (elem: HTMLElement | null): boolean => {
		// 	while (elem && elem !== this[0]) {
		// 		if (elem.scrollTop !== 0) {
		// 			return false;
		// 		}
		// 		elem = elem.parentElement;
		// 	}
		// 	if (elem) {
		// 		return elem.scrollTop === 0;
		// 	}
		// 	return false;
		// };

		let Y: number;
		let downID: number | null = null;
		let maxH: number;
		let isTouch = false;
		let ready = false;
		let refreshing = false;
		let top: number | null = null;
		let canRefresh = false;
		this.on('pointerdown', (evt) => {
			if (refreshing) {
				return;
			}
			isTouch = false;
			ready = false;
			let onSide = false;
			if (evt.target) {
				// prevent touch on scrollbar area
				const x = evt.offsetX + getOffsetX(evt.target as HTMLElement, this.elems()[0] as HTMLElement);
				if (x > this.width() - 20) {
					onSide = true;
				}
			}
			if (evt.isPrimary && !onSide) {
				// evt.preventDefault();
				// evt.stopImmediatePropagation();
				Y = evt.clientY;
				downID = evt.pointerId;
				top = this.elems()[0].scrollTop;
				canRefresh = top === 0;
				maxH = Math.floor(rem2px(1) * 10);
			}
		});
		this.on('touchstart', () => {
			isTouch = true;
		});
		this.on('contextmenu', (evt) => {
			evt.preventDefault();
		});
		this.on(
			'touchmove',
			(evt) => {
				if (evt.touches[0].clientY > Y && canRefresh) {
					if (evt.cancelable) {
						evt.preventDefault();
					}
					evt.stopImmediatePropagation();
				}
			},
			true
		);
		this.on(
			'click',
			(evt) => {
				if (moved) {
					evt.stopPropagation();
				}
				moved = false;
			},
			true
		);
		this.on('pointercancel', onCancel);
		this.on('pointerup', doRefresh);
		this.on(
			'pointermove',
			(evt) => {
				if (downID != null && downID === evt.pointerId) {
					let h = evt.clientY - 30 - Y;
					if (h > 0) {
						if (canRefresh /*!isTouch || noScroll(evt.target as HTMLElement)*/) {
							(this[0] as HTMLElement).setPointerCapture(downID);
							evt.stopImmediatePropagation();
							evt.preventDefault();
						} else {
							if (this[DATA].mouseScroll && !isTouch && top != null) {
								this.elems()[0].scrollTop = top - (evt.clientY - Y);
							}
							return;
						}
						if (h > maxH) {
							const diff = Math.min(h - maxH, maxH);
							const m = (Math.log(diff) / Math.log(maxH)) * (diff / 2);
							h = Math.floor(maxH + m);
							if (!ready) {
								ready = true;
								arrow.attr('class', 'ready');
							}
						} else {
							if (ready) {
								ready = false;
								arrow.attr('class', null);
							}
						}
					}
					if (h < 0) h = 0;
					canRefresh && move(h);
					if (this[DATA].mouseScroll && !isTouch && top != null) {
						this.elems()[0].scrollTop = top - (evt.clientY - Y);
					}
				}
			},
			true
		);
		const insObserver = new IntersectionObserver(
			(entries) => {
				if (entries[0].isIntersecting) {
					this.emit(new Event('scrollbottom', { bubbles: false }));
				}
			},
			{ root: this.elems()[0] }
		);
		insObserver.observe(anchor.elems()[0]);
	}

	onRefresh(proc: ((this: this) => Promise<void>) | null): this {
		this.refreshHandler = proc?.bind(this) ?? null;
		return this;
	}

	scrollToBottom(smooth = true): this {
		this.elems()[0].scroll({ top: this.scrollHeight(), behavior: smooth ? 'smooth' : 'instant' });
		return this;
	}

	mouseScroll(): boolean;
	mouseScroll(value: boolean): this;
	mouseScroll(value?: boolean): boolean | this {
		if (value === undefined) {
			return this[DATA].mouseScroll;
		}
		this[DATA].mouseScroll = value;
		return this;
	}

	protected get box(): Nodes {
		return this.query(':scope>div.content');
	}
}

export function pulldown(...className: string[]): Pulldown {
	return new Pulldown().addClass(...className);
}
